package at.ltasat.classifier.knn;

import java.io.IOException;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import at.ltasat.classifier.ObjectFeaturesFactory;
import at.ltasat.classifier.ClazzManager;
import at.ltasat.classifier.Classifier;
import at.ltasat.classifier.ClassifierFactory;
import at.ltasat.classifier.Clazz;
import at.ltasat.classifier.Membership;
import at.ltasat.classifier.Trainer;
import static at.ltasat.util.IO.*;

public final class KNearestNeighbourClassifierFactory<O, D> implements ClassifierFactory<O, D> {

	public static final String NAME = "knn";
	private static final int K = 5, FACTOR = 100;

	private final int k, factor;

	public KNearestNeighbourClassifierFactory() {
		this(K, FACTOR);
	}

	public KNearestNeighbourClassifierFactory(int k, int factor) {
		assert k >= 1;
		assert factor >= 1;
		this.k = k;
		this.factor = factor;
	}

	@Override
	public String getName() {
		return NAME;
	}

	@Override
	public Trainer<O, D> createTrainer(ClazzManager<D> classes, ObjectFeaturesFactory<O> factory) {
		return new KNearestNeighbourTrainer<O, D>(NAME, classes, factory, k, factor);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Classifier<O, D> readClassifier(final ClazzManager<D> classes, final ObjectFeaturesFactory<O> factory, final InputStream in) throws IOException {
		final int nDimensions = readInteger(in);
		if (nDimensions != factory.getNumberOfFeatures()) {
			throw new IOException("Number of dimensions must match value count in AttributedObjectFactory");
		}
		final int factor = readInteger(in);
		final int dataSize = readInteger(in);
		Map<Point, Membership<D>[]> data = new HashMap<Point, Membership<D>[]>(dataSize);
		for (int i = 0; i < dataSize; i++) {
			final Point point = readPoint(in, nDimensions);
			final Membership<?>[] memberships = new Membership<?>[readInteger(in)];
			for (int j = 0; j < memberships.length; j++) {
				final Clazz<D> clazz = classes.get(readInteger(in));
				final double degree = readDouble(in);
				memberships[j] = new Membership<D>(clazz, degree);
			}
			if (data.containsKey(point)) {
				throw new IOException("Data for " + point + " already read!");
			}
			data.put(point, (Membership<D>[]) memberships);
		}
		return new KNearestNeighbourClassifier<O, D>(NAME, classes, factory, K, factor, data);
	}

	static <O, D> void writeClassifier(final OutputStream out, final KNearestNeighbourClassifier<O, D> classifier) throws IOException {
		writeInteger(out, classifier.getNumberOfDimensions());
		writeInteger(out, classifier.getFactor());
		writeInteger(out, classifier.getDataSize());
		for (Point point : classifier.getDataPoints()) {
			writePoint(out, point);
			final Membership<?>[] memberships = classifier.getData(point);
			writeInteger(out, memberships.length);
			for (int i = 0; i < memberships.length; i++) {
				final Membership<?> membership = memberships[i];
				writeInteger(out, membership.getClazz().getId());
				assert 0 <= membership.getDegree() && membership.getDegree() <= 1;
				writeDouble(out, membership.getDegree());
			}
		}
	}

	private static void writePoint(final OutputStream out, final Point point) throws IOException {
		for (int i = 0; i < point.getNumberOfDimensions(); i++) {
			writeInteger(out, point.getCoordinate(i));
		}
	}

	private static Point readPoint(final InputStream in, final int dimension) throws IOException {
		final int[] coordinates = new int[dimension];
		for (int i = 0; i < coordinates.length; i++) {
			coordinates[i] = readInteger(in);
		}
		return new Point(coordinates);
	}
}
